package routers

import (
	"encoding/json"
	"io/ioutil"
	"law-api-master/log"
	"law-api-master/trust"
	apiTypes "law-api-master/types"
	"net/http"
	"sync"
)


//ChainRequest receive upload-data request from accounts
type ChainRequest struct {
	//Url     string              `json:"url"`
	//Privkey string              `json:"private_key"`
	Data    []*ChainRequestData `json:"data"`
}

//ChainRequestData fill what a account would storage in picoChain
type ChainRequestData struct {
	//AppID       string `json:"app_id"`
	//AccessToken string `json:"access_token"`
	FileID    string `json:"file_id"`
	Sha256    string `json:"sha256"`
	Sm3       string `json:"sm3"`
	Md5       string `json:"md5"`
	Timestamp string `json:"request_timestamp"`
}

//ChainResponse return status about upload request
type ChainResponse struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
}

//newChainResponse return result code and its error string
func newChainResponse(err error) *ChainResponse {
	//check upload result
	code := checkUploadErrCode(err)

	//if code isn't 0, return its error message
	msg := "成功"
	if code != 0 {
		msg = err.Error()
	}
	return &ChainResponse{Code: code, Msg: msg}
}

//uploadEvent post upload-event to inside gorountine
type uploadEvent struct {
	url  string
	data []*ChainData
}

//ChainDataResponse return data should be posted to callback url
type ChainDataResponse struct {
	Data []*ChainData `json:"data"`
}

//ChainData return picoChain result after upload request successfully.
type ChainData struct {
	Code        int              `json:"code"`
	FileID      string           `json:"file_id"`
	BlockNumber string           `json:"block_height"`
	BlockHash   string           `json:"block_hash"`
	TxHash      string           `json:"tran_hash"`
	Timestamp   string           `json:"block_timestamp"`
	TrustTime   *trust.ProofTime `json:"trust_timestamp"`
}

func newChainData(code int, fileID, bNum, bHash, txHash, timestamp string) *ChainData {
	return &ChainData{
		Code:        code,
		FileID:      fileID,
		BlockNumber: bNum,
		BlockHash:   bHash,
		TxHash:      txHash,
		Timestamp:   timestamp,
	}
}

//ChainHandler manager how to transfer request to picoChain
type ChainHandler struct {
	chain Chain

	//http request event
	uploadCh chan *ChainRequest
	resultCh chan *uploadEvent

	//trust time cache
	trustMux  sync.Mutex
	exitTrust map[string]*trust.ProofTime

	//upload request status
	pendingTask    map[string]int
	pendingTaskMux sync.Mutex

	quit chan struct{}
}

//NewChainHandler is a handler to deal upload request
func NewChainHandler(chain Chain) *ChainHandler {
	ch := &ChainHandler{
		chain:       chain,
		uploadCh:    make(chan *ChainRequest, 1000),
		resultCh:    make(chan *uploadEvent, 1000),
		pendingTask: make(map[string]int),
		exitTrust:   make(map[string]*trust.ProofTime),
		quit:        make(chan struct{}),
	}

	go ch.loop()
	return ch
}

//String show the main function of chain hanlder
func (ch *ChainHandler) String() string {
	return "upload account to picoChain"
}

//ServeHTTP check upload request,then post upload event
func (ch *ChainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Method == "POST" {

		w.Header().Add("Content-Type", "application/json;charset=utf-8")

		b, err := ioutil.ReadAll(r.Body)
		if err != nil {
			log.Warn("chainHandler", "err", err)
			w.Write(encRespon(newChainResponse(ErrHTTPRequest)))
			return
		}
		defer r.Body.Close()

		//parse request
		req := new(ChainRequest)
		if err = json.Unmarshal(b, req); err != nil {
			log.Warn("chainHandler", "err", err)
			w.Write(encRespon(newChainResponse(ErrHTTPRequest)))
			return
		}

		//check callback url
		if req.Url == "" {
			log.Warn("chainHandler", "err", "empty callback")
			w.Write(encRespon(newChainResponse(ErrHTTPRequest)))
			return
		}

		//check key
		if succeed := ch.chain.CheckPrivateKey(req.Privkey); succeed != true {
			log.Warn("chainHandler", "err", err)
			w.Write(encRespon(newChainResponse(ErrAccKey)))
			return
		}

		//check chain data
		for _, dat := range req.Data {
			if dat.FileID == "" || dat.Sha256 == "" || dat.Sm3 == "" || dat.Md5 == "" || dat.Timestamp == "" {
				log.Warn("chainHandler", "err", "empty chaindata")
				w.Write(encRespon(newChainResponse(ErrChainData)))
				return
			}
		}

		//post upload request event
		ch.PostUploadReq(req)
		w.Write(encRespon(newChainResponse(nil)))

	} else {
		log.Warn("chainHandler", "unknown request", r.Method)
		w.Write(encRespon(newChainResponse(ErrHTTPRequest)))
	}
}

//PostUploadReq post pending upload request event.
func (ch *ChainHandler) PostUploadReq(req *ChainRequest) error {
	select {
	case ch.uploadCh <- req:
	case <-ch.quit:
	}
	return nil
}

//PostUploadRes post done upload request event.
func (ch *ChainHandler) PostUploadRes(url string, data []*ChainData) error {
	select {
	case ch.resultCh <- &uploadEvent{url: url, data: data}:
	case <-ch.quit:
	}
	return nil
}

//Close stop handler
func (ch *ChainHandler) Close() {
	close(ch.quit)
}

//loop wait event
func (ch *ChainHandler) loop() {
	for {
		select {
		case req := <-ch.uploadCh:
			go ch.handleUpload(req)

		case event := <-ch.resultCh:
			res := &ChainDataResponse{Data: event.data}

			//post until succeed or arrive 50 time.
			go CallbackResponse(event.url, res)

			//when exitTrust too large,clear it
			if len(ch.exitTrust) >= 10000 {
				ch.exitTrust = make(map[string]*trust.ProofTime)
			}

		case <-ch.quit:
			return
		}
	}
}

//handleUpload send upload data to picoChain. After blocked,it will post done event
func (ch *ChainHandler) handleUpload(req *ChainRequest) {
	//upload data to the picoChain
	chDat := ch.upload(req)
	//post upload result,update pendig task
	ch.PostUploadRes(req.Url, chDat)
}

func (ch *ChainHandler) upload(req *ChainRequest) []*ChainData {
	//result status
	result := make(chan *ChainData, len(req.Data))
	datas := make([]*ChainData, 0)

	//recording pending task
	ch.addPendingTask(req.Privkey, len(req.Data))

	//upload chain
	timeout := len(req.Data)/100 + 1
	for _, dat := range req.Data {
		//each request data upload blockchain seperately.
		go func(data *ChainRequestData) {

			//upload data and wait result
			uploadDat := apiTypes.NewProofData(data.FileID, data.Sha256, data.Sm3,
				data.Md5, data.Timestamp)
			uploadRes, err := ch.chain.Upload(req.Privkey, uploadDat, timeout)

			//response data
			code := checkUploadErrCode(err)
			if err != nil {
				log.Warn("upload data", "fileid", data.FileID, "err", err)
				result <- &ChainData{Code: code, FileID: data.FileID}
				return
			}
			result <- newChainData(code, data.FileID, uploadRes.BlockNumber(),
				uploadRes.BlockHash(), uploadRes.TxHash(), uploadRes.Timestamp())

		}(dat)
	}

	//wait and stat result
	for i := 0; i < len(req.Data); i++ {
		data := <-result

		//set trust time after other origanizations accept our chain status
		ch.setTrustTime(data)
		datas = append(datas, data)
	}
	//all requst finish uploading, delete his pending task in this time
	ch.delPendingTask(req.Privkey, len(req.Data))

	return datas
}

func (ch *ChainHandler) addPendingTask(key string, times int) {
	ch.pendingTaskMux.Lock()
	defer ch.pendingTaskMux.Unlock()

	ch.pendingTask[key] = ch.pendingTask[key] + times
}

func (ch *ChainHandler) delPendingTask(key string, times int) {
	ch.pendingTaskMux.Lock()
	defer ch.pendingTaskMux.Unlock()

	ch.pendingTask[key] = ch.pendingTask[key] - times

	//when user stop uploading data, clear his historical data
	if ch.pendingTask[key] <= 0 {
		ch.chain.ClearUserNonce(key)
		delete(ch.pendingTask, key)
	}

}

//setTrustTime new trust timestamp for proofing block data really exits.
//it was empty in timestamp when return a error result.
func (ch *ChainHandler) setTrustTime(data *ChainData) bool {
	//check data validity
	if data.Code != 0 {
		return false
	}
	//haven't got trust time
	if _, ok := ch.exitTrust[data.BlockNumber]; !ok && data.Timestamp != "" {
		//try to get trust time
		if trustTime, err := trust.NewProofTime(data.Timestamp, data.BlockHash); err != nil {
			log.Warn("trust time", "blockNumber", data.BlockNumber, "err", err)
		} else {
			ch.exitTrust[data.BlockNumber] = trustTime
		}
	}
	data.TrustTime = ch.exitTrust[data.BlockNumber]
	return true
}
